<!--
 * @Description: 加购的跳动小球
 * @Author: Kerwin
 * @Date: 2024-07-22 15:56:25
 * @LastEditTime: 2024-07-24 13:46:08
 * @LastEditors:  Please set LastEditors
-->
<template>
    <view class="l-ball-wrapper" ref="l-ball-wrapper">
        <block v-for="item in balls" :key="item.time">
            <view v-if="!item.done" class="l-ball" :ref="item.ref" :style="item.style" :animation="item.data"></view>
        </block>
        <slot :active="active"></slot>
    </view>
</template>
<script lang="ts" setup>
import { onMounted, getCurrentInstance, reactive, computed, ref, Ref } from 'vue';
import { getRect, addUnit, getStyleStr, sleep, createAnimation } from './ballUtill';
const name = 'l-ball'

const props = defineProps({
    duration: {
        type: Number,
        default: 600
    },
    offset: {
        type: [Number, Array],
        default: 150
    },
    size: {
        type: [Number, String],
    },
    color: String
})

function useList(time: number = 600): [Ref<any[]>, () => void] {
    const balls = ref([])
    let timer = null
    const change = () => {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            const flag = balls.value.every((ball) => ball.done)
            if (flag) { balls.value = [] }
        }, time * 2.5)
    }
    return [balls, change]
}

function useState(): [Ref<boolean>, () => void] {
    const state = ref(false)
    let timer = null
    const change = () => {
        state.value = true
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            state.value = false
        }, 300)
    }
    return [state, change]
}

interface Point {
    x: number
    y: number
}
interface StartPoint extends TouchEvent {
    x?: number
    y?: number
}
const context = getCurrentInstance()
const target = reactive({ x: 0, y: 0 })
const numPoints = 30;
const [active, change] = useState()
const [balls, update] = useList(props.duration)

const calculateBezierPoint = (t: number, p0: Point, p1: Point, p2: Point) => {
    const q0x = p0.x + (p1.x - p0.x) * t;
    const q0y = p0.y + (p1.y - p0.y) * t;

    const q1x = p1.x + (p2.x - p1.x) * t;
    const q1y = p1.y + (p2.y - p1.y) * t;

    const bx = q0x + (q1x - q0x) * t;
    const by = q0y + (q1y - q0y) * t;
    return { x: bx, y: by };
}

const offset = computed(() => {
    const { offset } = props
    if (Array.isArray(offset)) {
        return offset as number[]
    } else {
        return [offset, offset]
    }
})

const action = async (start: StartPoint, end: Point = target) => {
    const time = +new Date()
    const { x, y, touches } = start
    const { clientX, clientY } = touches && touches['0'] || {}
    // 起点
    const p0 = {
        x: x || clientX,
        y: y || clientY,
    }
    // 投掷高度
    const p1 = {
        x: p0.x - end.x > offset.value[0] * 0.3 ? -offset.value[0] : offset.value[0],
        y: - offset.value[1]
    }
    // 终点
    const p2 = {
        x: end.x - p0.x,
        y: end.y - p0.y,
    }

    const ball = reactive({
        ref: `l-ball-${time}`,
        time,
        data: null,
        style: getStyleStr({
            left: addUnit(p0.x),
            top: addUnit(p0.y),
            width: addUnit(props.size),
            height: addUnit(props.size),
            backgroundColor: props.color,
        }),
        done: false
    })

    balls.value.push(ball)
    p0.x = 0
    p0.y = 0

    // 抛物线长度
    let length = 0
    let animation = null
    const points = []

    animation = createAnimation({
        duration: 0,
        timingFunction: 'linear'
    })

    for (let i = 0; i <= numPoints; i++) {
        const t = i / numPoints;
        const { x, y } = calculateBezierPoint(t, p0, p1, p2);
        let dist = 0
        if (points.length > 0) {
            const prevPoint = points[points.length - 1];
            dist = Math.sqrt((x - prevPoint.x) ** 2 + (y - prevPoint.y) ** 2);
            length += dist;
        }
        points.push({ x, y, d: dist })
    }
    points.forEach(({ x, y, d }, i) => {
        // 由于微信小程序duration 为 0 时整段动画都不执行，故默认值为1
        //#ifndef APP-NVUE
        let duration = (d / length * props.duration) || 1
        // #endif

        // 由于nvue过渡太慢故设为0
        //#ifdef APP-NVUE
        duration = 0
        // #endif

        // animation.opacity(1 - 1 / numPoints * i).left(x).top(y).step({duration: 300})
        // 由于微信小程序createAnimation 使用位置无过渡动画，故改成translate
        animation.opacity(1 - 1 / numPoints * i).translate(x, y).step({ duration })
    })

    // 由于微信小程序无法在生成节点时就执行动画，故在节点生成后再动态赋值
    await sleep(50)
    let totalDuration = props.duration
    let ref = null
    //#ifdef APP-NVUE
    // nvue里for出来的ref是个数组
    ref = context.refs && context.refs[ball.ref][0]
    // #endif
    ball.data = animation.export(ref)
    // 提前让接收的目标有个动画
    sleep(totalDuration * 0.52).then(() => {
        change()
    })
    sleep(totalDuration * 2).then(() => {
        ball.done = true
        update()
    })
}


onMounted(() => {
    sleep(150).then(() => {
        getRect(`.${name}-wrapper`, { context }).then(res => {
            target.x = res.left + res.width / 2
            target.y = res.top + res.height / 2
        })
    })
})
defineExpose({
    action
})

</script>
<style lang="scss">
$ball-size: 30rpx;
$ball-color: red;

.l-ball {
    width: $ball-size;
    height: $ball-size;
    background-color: $ball-color;
    position: fixed;
    border-radius: 999rpx;
    opacity: 0;
    z-index: 99;

    &-wrapper {
        /* #ifndef APP-NVUE */
        display: inline-block;
        /* #endif */
    }
}
</style>